package com.ea.admin.service.impl;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.UUID;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ea.admin.mapper.MysqlBackupMapper;
import com.ea.admin.pojo.po.MysqlBackup;
import com.ea.admin.pojo.vo.superAdmin.BackUpVo;
import com.ea.admin.pojo.vo.superAdmin.GetDatabaseInfoVo;
import com.ea.admin.service.DatabaseService;
import com.ea.admin.service.FileService;
import com.ea.common.auth.AdminFeign;
import com.ea.common.config.MySqlConfig;
import com.ea.common.report.ReportFeign;
import com.ea.common.result.BizException;
import com.ea.common.result.ErrorCode;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.exec.*;
import org.springframework.stereotype.Service;
import org.yaml.snakeyaml.Yaml;

import javax.annotation.Resource;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.time.Duration;
import java.time.LocalDate;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

@Service
@Slf4j
public class DatabaseServiceImpl implements DatabaseService {
    @Resource
    MysqlBackupMapper mysqlBackupMapper;
    @Resource
    AdminFeign adminFeign;
    @Resource
    ReportFeign reportFeign;
    @Resource
    MySqlConfig mySqlConfig;

    // 锁，防止并发备份
    private final Lock lock = new ReentrantLock();

    @Override
    public void editDatabaseUrl(String url, String username, String password) {
        // 测试能否链接数据库
        testConnect(url, username, password);

        // 备份原有文件
        String fileName = backupDatabaseReturnFileName();

        log.info("开始刷新数据库");
        // 保存修改
        try {
            editConfig(url, username, password);
        } catch (IOException e) {
            throw new BizException(ErrorCode.SYSTEM_ERROR);
        }
        // 重启服务
        // 调用feign的方法，刷新两个服务
        List<String> refresh = adminFeign.refresh();
        log.info("admin服务刷新结果: {}", refresh);
        refresh = reportFeign.refresh();
        log.info("report服务刷新结果: {}", refresh);

        recoverDatabaseByFileName(fileName, url, username, password);
    }

    private void testConnect(String url, String username, String password) {
        String mysqlDumpPath = fileService.getPathByType("root") + "mysql_dump" + File.separator + "bin";
        CommandLine commandLine = new CommandLine(mysqlDumpPath + File.separator + "mysql");
        commandLine.addArgument("-h" + parseUrlTo(url, "host"));
        commandLine.addArgument("-u" + username);
        commandLine.addArgument("-p" + password);
        commandLine.addArgument(parseUrlTo(url, "dbName"));
        DefaultExecutor executor = new DefaultExecutor();
        try {
            int exitCode = executor.execute(commandLine);
            if (executor.isFailure(exitCode)) {
                throw new BizException(ErrorCode.SYSTEM_ERROR_EDIT_MYSQL_CONFIG_ERROR);
            }
        } catch (Exception e) {
            throw new BizException(ErrorCode.SYSTEM_ERROR_EDIT_MYSQL_CONFIG_ERROR);
        }

    }

    public void editConfig(String url, String username, String password) throws IOException {
        String src = fileService.getPathByType("yml") + "application-pro.yml";
        log.info("修改配置文件: {}", src);
        updateYml(src, url, username, password);
        // 把 admin 替换为 report
        src = src.replace("admin", "report");
        log.info("修改配置文件: {}", src);
        updateYml(src, url, username, password);
    }

    public void updateYml(String path, String url, String username, String password) throws IOException {
        Yaml yaml = new Yaml();
        FileWriter fileWriter;
        FileInputStream fileInputStream = new FileInputStream(path);
        Map<String, Object> yamlMap = yaml.load(fileInputStream);
        Map<String, Object> springMap = (Map<String, Object>) yamlMap.get("spring");
        Map<String, Object> datasourceMap = (Map<String, Object>) springMap.get("datasource");
        log.info("修改前的url:{}", datasourceMap.get("url"));
        datasourceMap.put("url", url);
        datasourceMap.put("username", username);
        datasourceMap.put("password", password);
        //字符输出
        fileWriter = new FileWriter(path);
        //用yaml方法把map结构格式化为yaml文件结构
        fileWriter.write(yaml.dumpAsMap(yamlMap));
        //刷新
        fileWriter.flush();
        //关闭流
        fileWriter.close();
        fileInputStream.close();
    }


    @Override
    public GetDatabaseInfoVo getDatabaseInfoVo(Integer page, Integer pageSize) {
        // 获得数据库链接地址
        GetDatabaseInfoVo vo = new GetDatabaseInfoVo();
        vo.setUrl(mySqlConfig.getUrl());
        vo.setUsername(mySqlConfig.getUsername());
        LambdaQueryWrapper<MysqlBackup> wrapper = new LambdaQueryWrapper<>();
        wrapper.orderByDesc(MysqlBackup::getCreateTime);
        Page<MysqlBackup> mysqlBackupPage = mysqlBackupMapper.selectPage(new Page<>(page, pageSize), wrapper);
        vo.setItems(parseMysqlBackupListToVo(mysqlBackupPage.getRecords()));
        vo.setTotal((int) mysqlBackupPage.getTotal());
        return vo;
    }

    private List<BackUpVo> parseMysqlBackupListToVo(List<MysqlBackup> records) {
        return records.stream().map(this::parseMysqlBackupToVo)
                .collect(Collectors.toList());
    }

    private BackUpVo parseMysqlBackupToVo(MysqlBackup backup) {
        BackUpVo vo = new BackUpVo();
        vo.setMysqlBackupId(backup.getMysqlBackupId());
        vo.setSourceIp(backup.getSourceIp());
        vo.setSourcePort(backup.getSourcePort());
        vo.setBackupName(backup.getBackupName());
        vo.setRecoveryTime(backup.getRecoveryTime());
        vo.setCreateTime(backup.getCreateTime());
        vo.setIsAuto(backup.getIsAuto());
        return vo;
    }

    @Override
    public void recoverDatabase(Long mysqlBackupId) {
        // 获得mysql备份
        MysqlBackup mysqlBackup = mysqlBackupMapper.selectById(mysqlBackupId);
        if (mysqlBackup == null) {
            throw new BizException(ErrorCode.SYSTEM_RESOURCE_ERROR_DATABASE_BACKUP_NOT_EXIST);
        }
        recoverDatabaseByFileName(mysqlBackup.getFileName(), mySqlConfig.getUrl(), mySqlConfig.getUsername(), mySqlConfig.getPassword());

        // 更新数据库
        mysqlBackup.setRecoveryTime(new Date());
        mysqlBackupMapper.updateById(mysqlBackup);
    }

    private void recoverDatabaseByFileName(String fileName, String url, String username, String password) {
        log.info("还原备份：{}", fileName);
        // 还原备份文件
        String dir = fileService.getPathByType("sql");
        String backupPath = dir + fileName;
        File sqlFile = new File(backupPath);
        if (!sqlFile.exists()) {
            throw new BizException(ErrorCode.SYSTEM_RESOURCE_ERROR_FILE_NOT_EXISTS);
        }
        // 还原数据库
        //mysql -uroot -pEmoAnaly -h139.159.134.111 backup_test
        // < \maven\mysql\backup\sql\2024-04-02f4062eb9-9358-4960-a24c-2e57b6669ff2.sql

        InputStream stdIn = null;
        ByteArrayOutputStream stdOut = null;
        ByteArrayOutputStream stdErr = null;
        try {
            stdIn = new BufferedInputStream(Files.newInputStream(Paths.get(sqlFile.getPath())));
            stdOut = new ByteArrayOutputStream();
            stdErr = new ByteArrayOutputStream();
            String mysqlDumpPath = fileService.getPathByType("root") + "mysql_dump" + File.separator + "bin";
            CommandLine commandLine = new CommandLine(mysqlDumpPath + File.separator + "mysql");
            String host = parseUrlTo(url, "host");
            String dbName = parseUrlTo(url, "dbName");
            log.info("host: {}, dbName: {}", host, dbName);
            commandLine.addArgument("-h" + host);
            commandLine.addArgument("-u" + username);    // 用户名
            commandLine.addArgument("-p" + password);    // 密码
            commandLine.addArgument(dbName);

            ExecuteWatchdog watchdog = ExecuteWatchdog.builder().setTimeout(Duration.ofMillis(180 * 60)).get();
            ExecuteStreamHandler streamHandler = new PumpStreamHandler(stdOut, stdErr, stdIn);
            DefaultExecutor executor = DefaultExecutor.builder().setExecuteStreamHandler(streamHandler).get();
            executor.setWatchdog(watchdog);

            System.out.println(commandLine);
            int exitCode = executor.execute(commandLine);
            if (executor.isFailure(exitCode)) {
                System.out.println("执行失败");
            }

        } catch (ExecuteException e) {
            throw new BizException(ErrorCode.SYSTEM_ERROR_BACKUP_DATABASE_ERROR);
        } catch (Exception e) {
            throw new BizException(ErrorCode.SYSTEM_ERROR);
        } finally {
            // 关闭流
            if (stdIn != null) {
                try {
                    stdIn.close();
                } catch (IOException ignored) {
                }
            }
            if (stdOut != null) {
                try {
                    stdOut.close();
                } catch (IOException e) {
                }
            }
            if (stdErr != null) {
                try {
                    stdErr.close();
                } catch (IOException e) {
                }
            }
        }
    }


    @Resource
    FileService fileService;

    @Override
    public MysqlBackup backupDatabase(String backupName) {
        if (!this.lock.tryLock()) {
            throw new BizException(ErrorCode.SYSTEM_RESOURCE_ERROR_DATABASE_BACKUP_BUSY);
        }

        try {
            String fileName = backupDatabaseReturnFileName();
            // 插入数据库
            String url = mySqlConfig.getUrl();
            // 获取ip
            final String hostIp = parseUrlTo(url, "host");
            // 获取端口号
            final String port = parseUrlTo(url, "port");
            MysqlBackup mysqlBackup = new MysqlBackup();
            mysqlBackup.setSourceIp(hostIp);
            mysqlBackup.setSourcePort(port);
            mysqlBackup.setBackupName(backupName);
            mysqlBackup.setCreateTime(new Date());
            mysqlBackup.setFileName(fileName);
            mysqlBackup.setIsAuto(0);
            mysqlBackup.setIsDel(0);
            mysqlBackupMapper.insert(mysqlBackup);
            return mysqlBackup;
        } catch (Exception e) {
            throw new BizException(ErrorCode.SYSTEM_ERROR);
        } finally {
            this.lock.unlock();
        }
    }

    private String backupDatabaseReturnFileName() {
        String dir = fileService.getPathByType("sql");
        // 备份的SQL文件
        String fileName = String.valueOf(LocalDate.now()) + UUID.fastUUID() + ".sql";
        File sqlFile = new File(dir + File.separator + fileName);
        if (!FileUtil.exist(sqlFile)) {
            FileUtil.touch(sqlFile);
        }

        // 标准流输出的内容就是 SQL 的备份内容
        try {
            OutputStream stdOut = new BufferedOutputStream(
                    Files.newOutputStream(sqlFile.toPath(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING));
            // 监视狗。执行超时时间，1小时
            ExecuteWatchdog watchdog = new ExecuteWatchdog(TimeUnit.HOURS.toMillis(1));

            // 子进程执行器
            DefaultExecutor defaultExecutor = new DefaultExecutor();
            defaultExecutor.setWatchdog(watchdog);
            defaultExecutor.setStreamHandler(new PumpStreamHandler(stdOut, System.err));

            // 进程执行命令 mysqldump 在根目录下的maven文件夹中
            // /maven/mysql_dump/bin
            String mysqlDumpPath = fileService.getPathByType("root") + "mysql_dump" + File.separator + "bin";
            CommandLine commandLine = new CommandLine(mysqlDumpPath + File.separator + "mysqldump");
            String host = parseUrlTo(mySqlConfig.getUrl(), "host");
            String dbName = parseUrlTo(mySqlConfig.getUrl(), "dbName");
            commandLine.addArgument("-h" + host);
            commandLine.addArgument("-u" + mySqlConfig.getUsername());    // 用户名
            commandLine.addArgument("-p" + mySqlConfig.getPassword());    // 密码
            commandLine.addArgument(dbName);                // 数据库

            // 同步执行，阻塞直到子进程执行完毕。
            int exitCode = defaultExecutor.execute(commandLine);
            if (defaultExecutor.isFailure(exitCode)) {
                throw new Exception("备份任务执行异常：exitCode=" + exitCode);
            }
            return fileName;
        } catch (ExecuteException e) {
            throw new BizException(ErrorCode.SYSTEM_ERROR_BACKUP_DATABASE_ERROR);
        } catch (Exception e) {
            throw new BizException(ErrorCode.SYSTEM_ERROR);
        }
    }

    public String parseUrlTo(String url, String result) {
        Pattern p = Pattern.compile("jdbc:(?<db>\\w+):.*((//)|@)(?<host>.+):(?<port>\\d+)(/|(;DatabaseName=)|:)(?<dbName>\\w+)\\??.*");
        Matcher m = p.matcher(url);
        if (m.find()) {
            switch (result) {
                case "db":
                    return m.group("db");
                case "host":
                    return m.group("host");
                case "port":
                    return m.group("port");
                case "dbName":
                    return m.group("dbName");
            }
        }
        throw new BizException(ErrorCode.SYSTEM_ERROR_BACKUP_DATABASE_ERROR);
    }

}
